Gelişmiş TypeScript OOP desenlerini, sınıf tasarımını, kalıtım/kompozisyonu ve global uygulamalar için ölçeklenebilir stratejileri keşfedin.
TypeScript OOP Desenleri: Sınıf Tasarımı ve Kalıtım Stratejilerine Yönelik Bir Kılavuz
Modern yazılım geliştirme dünyasında TypeScript, sağlam, ölçeklenebilir ve bakımı kolay uygulamalar oluşturmak için bir temel taşı olarak ortaya çıkmıştır. JavaScript üzerine kurulu güçlü yazım sistemi, geliştiricilere hataları erken yakalama ve daha öngörülebilir kod yazma araçları sağlar. TypeScript'in gücünün merkezinde, Nesne Yönelimli Programlama (OOP) ilkelerine kapsamlı desteği yatmaktadır. Ancak, sadece bir sınıfın nasıl oluşturulacağını bilmek yeterli değildir. TypeScript'e hakim olmak, sınıf tasarımı, kalıtım hiyerarşileri ve farklı mimari desenler arasındaki ödünleşimleri derinlemesine anlamayı gerektirir.
Bu kılavuz, orta seviye becerilerini pekiştirenlerden deneyimli mimarlara kadar global bir geliştirici kitlesi için tasarlanmıştır. TypeScript'teki OOP'nin temel kavramlarına derinlemesine dalacak, etkili sınıf tasarım stratejilerini keşfedecek ve eskimeyen tartışmayı ele alacağız: kalıtım ve kompozisyon. Sonunda, daha temiz, daha esnek ve geleceğe hazır kod tabanları sağlayan bilinçli tasarım kararları verme bilgisine sahip olacaksınız.
TypeScript'te OOP'nin Temel Taşlarını Anlamak
Karmaşık desenlere dalmadan önce, TypeScript'e uygulandığı şekliyle Nesne Yönelimli Programlamanın dört temel direğini tekrar gözden geçirerek sağlam bir temel oluşturalım.
1. Kapsülleme
Kapsülleme, bir nesnenin verilerini (özelliklerini) ve bu veriler üzerinde işlem yapan metotları tek bir birim – bir sınıf – içinde paketleme ilkesidir. Aynı zamanda, bir nesnenin dahili durumuna doğrudan erişimi kısıtlamayı da içerir. TypeScript bunu öncelikli olarak erişim değiştiriciler aracılığıyla başarır: public, private ve protected.
Örnek: Bakiyenin sadece para yatırma ve çekme metotları aracılığıyla değiştirilebildiği bir banka hesabı.
class BankAccount {
private balance: number = 0;
constructor(initialBalance: number) {
if (initialBalance >= 0) {
this.balance = initialBalance;
}
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited: ${amount}. New balance: ${this.balance}`);
}
}
public getBalance(): number {
// We expose the balance through a method, not directly
return this.balance;
}
}
2. Soyutlama
Soyutlama, karmaşık uygulama detaylarını gizlemek ve bir nesnenin yalnızca temel özelliklerini ortaya koymak anlamına gelir. Altındaki karmaşık mekanizmayı anlamaya gerek kalmadan üst düzey kavramlarla çalışmamızı sağlar. TypeScript'te soyutlama genellikle abstract sınıflar ve interfaces kullanılarak elde edilir.
Örnek: Bir uzaktan kumanda kullandığınızda, sadece "Açma/Kapama" düğmesine basarsınız. Kızılötesi sinyalleri veya dahili devreyi bilmenize gerek yoktur. Kumanda, TV'nin işlevselliğine soyut bir arayüz sağlar.
3. Kalıtım
Kalıtım, yeni bir sınıfın (alt sınıf veya türetilmiş sınıf) mevcut bir sınıftan (üst sınıf veya temel sınıf) özellikler ve metotlar devraldığı bir mekanizmadır. Kodun yeniden kullanımını teşvik eder ve sınıflar arasında açık bir "bir ...dır" ilişkisi kurar. TypeScript, kalıtım için extends anahtar kelimesini kullanır.
Örnek: Bir `Yönetici`, bir `Çalışan` türüdür. `isim` ve `kimlik` gibi ortak özelliklere sahiptirler, ancak `Yönetici`nin `astlar` gibi ek özellikleri olabilir.
class Employee {
constructor(public name: string, public id: number) {}
getProfile(): string {
return `Name: ${this.name}, ID: ${this.id}`;
}
}
class Manager extends Employee {
constructor(name: string, id: number, public subordinates: Employee[]) {
super(name, id); // Call the parent constructor
}
// Managers can also have their own methods
delegateTask(): void {
console.log(`${this.name} is delegating tasks.`);
}
}
4. Polimorfizm
"Çok biçimlilik" anlamına gelen polimorfizm, farklı sınıflardan nesnelerin ortak bir üst sınıfın nesneleri olarak işlem görmesini sağlar. Tek bir arayüzün (bir metot adı gibi) farklı temel formları (uygulamaları) temsil etmesini mümkün kılar. Bu genellikle metot geçersiz kılma (method overriding) yoluyla başarılır.
Örnek: Hem `Circle` hem de `Square` birer `Shape` olmasına rağmen, bir `Circle` nesnesi için farklı, bir `Square` nesnesi için farklı davranan bir `render()` metodu.
abstract class Shape {
abstract draw(): void; // An abstract method must be implemented by subclasses
}
class Circle extends Shape {
draw(): void {
console.log("Drawing a circle.");
}
}
class Square extends Shape {
draw(): void {
console.log("Drawing a square.");
}
}
function renderShapes(shapes: Shape[]): void {
shapes.forEach(shape => shape.draw()); // Polymorphism in action!
}
const myShapes: Shape[] = [new Circle(), new Square()];
renderShapes(myShapes);
// Output:
// Drawing a circle.
// Drawing a square.
Büyük Tartışma: Kalıtım ve Kompozisyon
Bu, OOP'deki en kritik tasarım kararlarından biridir. Modern yazılım mühendisliğindeki yaygın kanaat "kalıtıma karşı kompozisyonu tercih etmektir." Her iki kavramı da derinlemesine inceleyerek nedenini anlayalım.
Kalıtım Nedir? "Bir ...dır" İlişkisi
Kalıtım, temel sınıf ile türetilmiş sınıf arasında sıkı bir bağımlılık oluşturur. extends kullandığınızda, yeni sınıfın temel sınıfın özelleşmiş bir versiyonu olduğunu belirtirsiniz. Net bir hiyerarşik ilişki olduğunda kodun yeniden kullanımı için güçlü bir araçtır.
- Artıları:
- Kod Tekrar Kullanımı: Ortak mantık temel sınıfta bir kez tanımlanır.
- Polimorfizm: `Shape` örneğimizde görüldüğü gibi, zarif, polimorfik davranışa izin verir.
- Açık Hiyerarşi: Gerçek dünya, yukarıdan aşağıya bir sınıflandırma sistemini modeller.
- Eksileri:
- Sıkı Bağlılık: Temel sınıftaki değişiklikler türetilmiş sınıfları istenmeden bozabilir. Bu, "kırılgan temel sınıf sorunu" olarak bilinir.
- Hiyerarşi Cehennemi: Aşırı kullanım, anlaşılması ve bakımı zor derin, karmaşık ve katı kalıtım zincirlerine yol açabilir.
- Esneklik Eksikliği: Bir sınıf TypeScript'te yalnızca bir başka sınıftan kalıtım alabilir (tekli kalıtım), bu da kısıtlayıcı olabilir. Birden fazla, ilişkisiz sınıftan özellikler devralamazsınız.
Kalıtım Ne Zaman İyi Bir Seçimdir?
İlişki gerçekten "bir ...dır" olduğunda ve stabil olup değişme olasılığı düşük olduğunda kalıtımı kullanın. Örneğin, `CheckingAccount` (Vadesiz Hesap) ve `SavingsAccount` (Tasarruf Hesabı) her ikisi de temel olarak `BankAccount` (Banka Hesabı) türleridir. Bu hiyerarşi mantıklıdır ve yeniden modellenecek olması pek olası değildir.
Kompozisyon Nedir? "Bir ...ya sahiptir" İlişkisi
Kompozisyon, karmaşık nesnelerin daha küçük, bağımsız nesnelerden oluşturulmasını içerir. Bir sınıfın başka bir şey olması yerine, gerekli işlevselliği sağlayan başka nesnelere sahip olması durumudur. Bu, sınıfın yalnızca oluşturulan nesnelerin genel arayüzüyle etkileşime girmesi nedeniyle gevşek bir bağlılık oluşturur.
- Artıları:
- Esneklik: İşlevsellik, bir araya getirilen nesneler değiştirilerek çalışma zamanında değiştirilebilir.
- Gevşek Bağlılık: İçeren sınıf, kullandığı bileşenlerin iç işleyişini bilmek zorunda değildir. Bu, kodu test etmeyi ve sürdürmeyi kolaylaştırır.
- Hiyerarşi Sorunlarından Kaçınma: Karışık bir kalıtım ağacı oluşturmadan çeşitli kaynaklardan işlevsellikleri birleştirebilirsiniz.
- Açık Sorumluluklar: Her bileşen sınıfı Tek Sorumluluk İlkesi'ne uyabilir.
- Eksileri:
- Daha Fazla Kalıp Kod: Bazen, basit bir kalıtım modeline kıyasla farklı bileşenleri bağlamak için daha fazla kod gerektirebilir.
- Hiyerarşiler İçin Daha Az Sezgisel: Doğal taksonomileri kalıtım kadar doğrudan modellemez.
Pratik Bir Örnek: Araba
Bir `Araba`, kompozisyonun mükemmel bir örneğidir. Bir `Araba` bir `Motor` türü değildir, ne de bir `Tekerlek` türüdür. Bunun yerine, bir `Araba` bir `Motor`a sahiptir ve `Tekerlekler`e sahiptir.
// Component classes
class Engine {
start() {
console.log("Engine starting...");
}
}
class GPS {
navigate(destination: string) {
console.log(`Navigating to ${destination}...`);
}
}
// The composite class
class Car {
private readonly engine: Engine;
private readonly gps: GPS;
constructor() {
// The Car creates its own parts
this.engine = new Engine();
this.gps = new GPS();
}
driveTo(destination: string) {
this.engine.start();
this.gps.navigate(destination);
console.log("Car is on its way.");
}
}
const myCar = new Car();
myCar.driveTo("New York City");
Bu tasarım son derece esnektir. Bir `ElectricEngine`e sahip bir `Araba` oluşturmak istersek, yeni bir kalıtım zincirine ihtiyacımız olmaz. `Araba`ya bileşenlerini sağlamak için Bağımlılık Enjeksiyonu (Dependency Injection) kullanabiliriz, bu da onu daha da modüler hale getirir.
interface IEngine {
start(): void;
}
class PetrolEngine implements IEngine {
start() { console.log("Petrol engine starting..."); }
}
class ElectricEngine implements IEngine {
start() { console.log("Silent electric engine starting..."); }
}
class AdvancedCar {
// The car depends on an abstraction (interface), not a concrete class
constructor(private engine: IEngine) {}
startJourney() {
this.engine.start();
console.log("Journey has begun.");
}
}
const tesla = new AdvancedCar(new ElectricEngine());
tesla.startJourney();
const ford = new AdvancedCar(new PetrolEngine());
ford.startJourney();
TypeScript'te Gelişmiş Stratejiler ve Desenler
Kalıtım ve kompozisyon arasındaki temel seçimin ötesinde, TypeScript sofistike ve esnek sınıf tasarımları oluşturmak için güçlü araçlar sağlar.
1. Soyut Sınıflar: Kalıtım İçin Bir Taslak
Güçlü bir "bir ...dır" ilişkiniz olduğunda ancak temel sınıfların kendi başlarına örneklendirilememesini sağlamak istediğinizde abstract sınıfları kullanın. Ortak metotları ve özellikleri tanımlayan bir taslak görevi görürler ve türetilmiş sınıfların uygulaması gereken abstract metotlar bildirebilirler.
Kullanım Durumu: Bir ödeme işleme sistemi. Her ağ geçidinin pay() ve refund() metotlarına sahip olması gerektiğini biliyorsunuz, ancak uygulama her sağlayıcıya özeldir (örn. Stripe, PayPal).
abstract class PaymentGateway {
constructor(public apiKey: string) {}
// A concrete method shared by all subclasses
protected connect(): void {
console.log("Connecting to payment service...");
}
// Abstract methods that subclasses must implement
abstract processPayment(amount: number): boolean;
abstract issueRefund(transactionId: string): boolean;
}
class StripeGateway extends PaymentGateway {
processPayment(amount: number): boolean {
this.connect();
console.log(`Processing ${amount} via Stripe.`);
return true;
}
issueRefund(transactionId: string): boolean {
console.log(`Refunding transaction ${transactionId} via Stripe.`);
return true;
}
}
// const gateway = new PaymentGateway("key"); // Error: Cannot create an instance of an abstract class.
const stripe = new StripeGateway("sk_test_123");
stripe.processPayment(100);
2. Arayüzler: Davranış İçin Sözleşmeleri Tanımlama
TypeScript'teki arayüzler, bir sınıfın şekli için bir sözleşme tanımlamanın bir yoludur. Bir sınıfın hangi özelliklere ve metotlara sahip olması gerektiğini belirtirler, ancak herhangi bir uygulama sağlamazlar. Bir sınıf birden fazla arayüzü implement edebilir, bu da onları kompozisyonel ve ayrık tasarımın temel taşı yapar.
Arayüz ve Soyut Sınıf Karşılaştırması
- Birbirine yakın ilişkili birkaç sınıf arasında uygulanmış kodu paylaşmak istediğinizde soyut bir sınıf kullanın.
- Farklı, ilişkisiz sınıflar tarafından uygulanabilecek bir davranış için bir sözleşme tanımlamak istediğinizde bir arayüz kullanın.
Kullanım Durumu: Bir sistemde, birçok farklı nesnenin bir dize formatına (örn. günlüğe kaydetme veya depolama için) serileştirilmesi gerekebilir. Bu nesneler (User, Product, Order) ilişkisizdir ancak ortak bir yeteneği paylaşırlar.
interface ISerializable {
serialize(): string;
}
class User implements ISerializable {
constructor(public id: number, public name: string) {}
serialize(): string {
return JSON.stringify({ id: this.id, name: this.name });
}
}
class Product implements ISerializable {
constructor(public sku: string, public price: number) {}
serialize(): string {
return JSON.stringify({ sku: this.sku, price: this.price });
}
}
function logItems(items: ISerializable[]): void {
items.forEach(item => {
console.log("Serialized item:", item.serialize());
});
}
const user = new User(1, "Alice");
const product = new Product("TSHIRT-RED", 19.99);
logItems([user, product]);
3. Mixin'ler: Kod Tekrar Kullanımına Kompozisyonel Bir Yaklaşım
TypeScript yalnızca tekli kalıtıma izin verdiğinden, birden fazla kaynaktan kodu yeniden kullanmak isterseniz ne olur? İşte burada mixin deseni devreye girer. Mixin'ler, bir kurucu (constructor) alan ve onu yeni işlevselliklerle genişleten yeni bir kurucu döndüren fonksiyonlardır. Bir sınıfa yetenekleri "karıştırmanıza" olanak tanıyan bir kompozisyon biçimidir.
Kullanım Durumu: Birden fazla model sınıfına `Timestamp` (createdAt, updatedAt ile) ve `SoftDeletable` (deletedAt özelliği ve softDelete() metodu ile) davranışlarını eklemek istiyorsunuz.
// A Type helper for mixins
type Constructor = new (...args: any[]) => T;
// Timestamp Mixin
function Timestamped(Base: TBase) {
return class extends Base {
createdAt: Date = new Date();
updatedAt: Date = new Date();
};
}
// SoftDeletable Mixin
function SoftDeletable(Base: TBase) {
return class extends Base {
deletedAt: Date | null = null;
softDelete() {
this.deletedAt = new Date();
console.log("Item has been soft deleted.");
}
};
}
// Base class
class DocumentModel {
constructor(public title: string) {}
}
// Create a new class by composing mixins
const UserAccountModel = SoftDeletable(Timestamped(DocumentModel));
const userAccount = new UserAccountModel("My User Account");
console.log(userAccount.title);
console.log(userAccount.createdAt);
userAccount.softDelete();
console.log(userAccount.deletedAt);
Sonuç: Geleceğe Yönelik TypeScript Uygulamaları Oluşturmak
TypeScript'te Nesne Yönelimli Programlamada ustalaşmak, sözdizimini anlamaktan tasarım felsefesini benimsemeye uzanan bir yolculuktur. Sınıf yapısı, kalıtım ve kompozisyonla ilgili yaptığınız seçimler, uygulamanızın uzun vadeli sağlığı üzerinde derin bir etkiye sahiptir.
Global geliştirme pratiğiniz için temel çıkarımlar şunlardır:
- Temel Taşlarla Başlayın: Kapsülleme, Soyutlama, Kalıtım ve Polimorfizm konularında sağlam bir kavrayışa sahip olduğunuzdan emin olun. Bunlar OOP'nin kelime dağarcığıdır.
- Kalıtıma Karşı Kompozisyonu Tercih Edin: Bu ilke sizi daha esnek, modüler ve test edilebilir koda götürecektir. Kompozisyonla başlayın ve yalnızca net, istikrarlı bir "bir ...dır" ilişkisi olduğunda kalıtıma başvurun.
- İş İçin Doğru Aracı Kullanın:
- İstikrarlı bir hiyerarşide gerçek özelleşme ve kod paylaşımı için Kalıtım kullanın.
- Bazı uygulamaları paylaşırken bir sözleşmeyi dayatan bir sınıf ailesi için ortak bir temel tanımlamak amacıyla Soyut Sınıflar kullanın.
- Herhangi bir sınıf tarafından uygulanabilecek davranışlar için sözleşmeleri tanımlamak amacıyla Arayüzler kullanın, aşırı ayrışmayı teşvik edin.
- Birden fazla kaynaktan işlevleri bir sınıfa birleştirmeniz gerektiğinde, tekli kalıtımın sınırlamalarını aşarak Mixin'ler kullanın.
Bu desenler hakkında eleştirel düşünerek ve ödünleşimlerini anlayarak, sadece bugün güçlü ve verimli olmakla kalmayıp, aynı zamanda dünyanın neresinde olursanız olun sizin veya ekibinizin önümüzdeki yıllarda kolayca uyarlayabileceği, genişletebileceği ve bakımını yapabileceği TypeScript uygulamaları tasarlayabilirsiniz.